import {DbClient, DbClientNotFoundError, DbConfig, Transaction, TxFun} from "./dbClient";
import {isArray, TableConfig} from "../common";

const mysql = require('mysql');

class MySqlTransaction extends Transaction {
    connection: any;

    constructor(connection: any) {
        super();
        this.connection = connection;
    }

    async oneOrNone(sql: string): Promise<any> {
        const result = await this.query(sql);
        return result ? (isArray(result) ? result[0] : result) : null;
    }

    async one(sql: string): Promise<any> {
        const result = await this.query(sql);
        if (!result) {
            throw new DbClientNotFoundError();
        }
        return isArray(result) ? result[0] : result;
    }

    async query(sql: string): Promise<any> {
        return new Promise((resolve, reject) => {
            this.connection.query(sql, (error: any, results: any) => {
                if (error) {
                    return reject(error);
                }
                resolve(results);
            });
        });
    }

    async none(sql: string): Promise<void> {
        await this.query(sql);
    }
}

export class MySql extends DbClient {
    private pool: any;
    private connectionLimit?: number;

    constructor({host, port, database, user, password, connectionLimit}: DbConfig) {
        super({host, port, database, user, password});
        this.connectionLimit = connectionLimit;
    }

    setConnection(connection: any): void {
        this.pool = connection;
    }

    async oneOrNone(sql: string, transaction?: MySqlTransaction): Promise<any> {
        const executor = transaction || this;
        const result = await executor.query(sql);
        return result ? (isArray(result) ? result[0] : result) : null;
    }

    async one(sql: string, transaction?: MySqlTransaction): Promise<any> {
        const executor = transaction || this;
        const result = await executor.query(sql);
        if (!result) {
            throw new DbClientNotFoundError();
        }
        return isArray(result) ? result[0] : result;
    }

    async query(sql: string, transaction?: MySqlTransaction): Promise<any> {
        return new Promise(async (resolve, reject) => {
            let connection: any;
            let shouldRelease = false;

            try {
                if (transaction) {
                    connection = transaction.connection;
                } else {
                    connection = await this.getConnection();
                    shouldRelease = true;
                }

                connection.query(sql, (error: any, results: any) => {
                    if (shouldRelease) {
                        connection.release();
                    }
                    if (error) {
                        return reject(error);
                    }
                    resolve(results);
                });
            } catch (error) {
                if (shouldRelease && connection) {
                    connection.release();
                }
                reject(error);
            }
        });
    }

    async none(sql: string, transaction?: MySqlTransaction): Promise<void> {
        await this.query(sql, transaction);
    }

    async tx(func: TxFun): Promise<any> {
        return new Promise(async (resolve, reject) => {
            const connection = await this.getConnection();

            connection.beginTransaction(async (transactionErr: any) => {
                if (transactionErr) {
                    connection.release();
                    return reject(transactionErr);
                }

                const transaction = new MySqlTransaction(connection);

                try {
                    const result = await func(transaction);
                    connection.commit((commitError: any) => {
                        connection.release();
                        if (commitError) {
                            return reject(commitError);
                        }
                        resolve(result);
                    });
                } catch (error) {
                    console.error('Transaction error:', error);
                    connection.rollback(() => {
                        connection.release();
                        reject(error);
                    });
                }
            });
        });
    }

    async connect(): Promise<void> {
        const {host, port, database, user, password, connectionLimit} = this;
        this.pool = mysql.createPool({
            host,
            port,
            database,
            user,
            password,
            connectionLimit: connectionLimit || 10,
            acquireTimeout: 60000,
            timeout: 60000,
            reconnect: true
        });
    }

    private async getConnection(): Promise<any> {
        return new Promise((resolve, reject) => {
            this.pool.getConnection((err: any, connection: any) => {
                if (err) {
                    return reject(err);
                }
                resolve(connection);
            });
        });
    }

    insertSql(data: any, cs: any, tableConfig: TableConfig): string {
        const columnNames = cs.columns.map((it: any) => it.name);
        const values = columnNames.map((it: string) => {
            return this.escapeValue(data[it]);
        });

        return `INSERT INTO ${tableConfig.table} (${columnNames.join(',')}) VALUES (${values.join(',')})`;
    }

    updateSql(data: any, cs: any, tableConfig: TableConfig): string {
        const setClause = cs.columns.map((it: any) => {
            return `${it.name} = ${this.escapeValue(data[it.name])}`;
        }).join(',');

        return `UPDATE ${tableConfig.table} SET ${setClause}`;
    }
}